/**
 * Copyright 2013-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 *
 * @providesModule ReactChildren
 */

'use strict'

var PooledClass = require('./PooledClass')
var ReactElement = require('./ReactElement')

var emptyFunction = require('fbjs/lib/emptyFunction')
var traverseAllChildren = require('./traverseAllChildren')

var twoArgumentPooler = PooledClass.twoArgumentPooler
var fourArgumentPooler = PooledClass.fourArgumentPooler

var userProvidedKeyEscapeRegex = /\/+/g
function escapeUserProvidedKey(text) {
  return ('' + text).replace(userProvidedKeyEscapeRegex, '$&/')
}

/**
 * PooledClass representing the bookkeeping associated with performing a child
 * traversal. Allows avoiding binding callbacks.
 *
 * @constructor ForEachBookKeeping
 * @param {!function} forEachFunction Function to perform traversal with.
 * @param {?*} forEachContext Context to perform context with.
 */
function ForEachBookKeeping(forEachFunction, forEachContext) {
  this.func = forEachFunction
  this.context = forEachContext
  this.count = 0
}
ForEachBookKeeping.prototype.destructor = function () {
  this.func = null
  this.context = null
  this.count = 0
}
PooledClass.addPoolingTo(ForEachBookKeeping, twoArgumentPooler)

function forEachSingleChild(bookKeeping, child, name) {
  var func = bookKeeping.func
  var context = bookKeeping.context

  func.call(context, child, bookKeeping.count++)
}

/**
 * Iterates through children that are typically specified as `props.children`.
 *
 * The provided forEachFunc(child, index) will be called for each
 * leaf child.
 *
 * @param {?*} children Children tree container.
 * @param {function(*, int)} forEachFunc
 * @param {*} forEachContext Context for forEachContext.
 */
function forEachChildren(children, forEachFunc, forEachContext) {
  if (children == null) {
    return children
  }
  var traverseContext = ForEachBookKeeping.getPooled(
    forEachFunc,
    forEachContext
  )
  traverseAllChildren(children, forEachSingleChild, traverseContext)
  ForEachBookKeeping.release(traverseContext)
}

/**
 * PooledClass representing the bookkeeping associated with performing a child
 * mapping. Allows avoiding binding callbacks.
 *
 * @constructor MapBookKeeping
 * @param {!*} mapResult Object containing the ordered map of results.
 * @param {!function} mapFunction Function to perform mapping with.
 * @param {?*} mapContext Context to perform mapping with.
 */
function MapBookKeeping(mapResult, keyPrefix, mapFunction, mapContext) {
  this.result = mapResult
  this.keyPrefix = keyPrefix
  this.func = mapFunction
  this.context = mapContext
  this.count = 0
}
MapBookKeeping.prototype.destructor = function () {
  this.result = null
  this.keyPrefix = null
  this.func = null
  this.context = null
  this.count = 0
}
PooledClass.addPoolingTo(MapBookKeeping, fourArgumentPooler)

function mapSingleChildIntoContext(bookKeeping, child, childKey) {
  var result = bookKeeping.result
  var keyPrefix = bookKeeping.keyPrefix
  var func = bookKeeping.func
  var context = bookKeeping.context

  var mappedChild = func.call(context, child, bookKeeping.count++)
  if (Array.isArray(mappedChild)) {
    mapIntoWithKeyPrefixInternal(
      mappedChild,
      result,
      childKey,
      emptyFunction.thatReturnsArgument
    )
  } else if (mappedChild != null) {
    if (ReactElement.isValidElement(mappedChild)) {
      mappedChild = ReactElement.cloneAndReplaceKey(
        mappedChild,
        // Keep both the (mapped) and old keys if they differ, just as
        // traverseAllChildren used to do for objects as children
        keyPrefix +
          (mappedChild.key && (!child || child.key !== mappedChild.key)
            ? escapeUserProvidedKey(mappedChild.key) + '/'
            : '') +
          childKey
      )
    }
    result.push(mappedChild)
  }
}

function mapIntoWithKeyPrefixInternal(children, array, prefix, func, context) {
  var escapedPrefix = ''
  if (prefix != null) {
    escapedPrefix = escapeUserProvidedKey(prefix) + '/'
  }
  var traverseContext = MapBookKeeping.getPooled(
    array,
    escapedPrefix,
    func,
    context
  )
  traverseAllChildren(children, mapSingleChildIntoContext, traverseContext)
  MapBookKeeping.release(traverseContext)
}

/**
 * Maps children that are typically specified as `props.children`.
 *
 * The provided mapFunction(child, key, index) will be called for each
 * leaf child.
 *
 * @param {?*} children Children tree container.
 * @param {function(*, int)} func The map function.
 * @param {*} context Context for mapFunction.
 * @return {object} Object containing the ordered map of results.
 */
function mapChildren(children, func, context) {
  if (children == null) {
    return children
  }
  var result = []
  mapIntoWithKeyPrefixInternal(children, result, null, func, context)
  return result
}

function forEachSingleChildDummy(traverseContext, child, name) {
  return null
}

/**
 * Count the number of children that are typically specified as
 * `props.children`.
 *
 * @param {?*} children Children tree container.
 * @return {number} The number of children.
 */
function countChildren(children, context) {
  return traverseAllChildren(children, forEachSingleChildDummy, null)
}

/**
 * Flatten a children object (typically specified as `props.children`) and
 * return an array with appropriately re-keyed children.
 */
function toArray(children) {
  var result = []
  mapIntoWithKeyPrefixInternal(
    children,
    result,
    null,
    emptyFunction.thatReturnsArgument
  )
  return result
}

var ReactChildren = {
  forEach: forEachChildren,
  map: mapChildren,
  mapIntoWithKeyPrefixInternal: mapIntoWithKeyPrefixInternal,
  count: countChildren,
  toArray: toArray
}

module.exports = ReactChildren
